Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6장. 문자열 다루기 기초

4장에서 문자열을 가볍게 소개했다. 이 장에서는 Go 의 문자열이 내부적으로 어떻게 생겼는지, 그리고 한글 같은 다국어 문자를 다룰 때 무엇을 조심해야 하는지 본다.

목표:

  • 문자열이 “바이트의 나열” 이라는 점 이해하기
  • 영문과 한글의 길이 차이를 설명할 수 있게 되기
  • byterune 의 쓰임새 구분하기

6.1 문자열의 본질

불변 (immutable)

Go 의 문자열은 한 번 만들어지면 내용을 바꿀 수 없다.

s := "hello"
s[0] = 'H'   // 컴파일 에러

문자열의 일부 글자를 바꾸고 싶다면 새 문자열을 만들어 변수에 다시 대입해야 한다.

s = "Hello"   // 새 문자열을 대입하는 건 가능

“변수가 가리키는 문자열을 통째로 교체” 하는 건 자유다. 다만 기존 문자열의 내부 글자만 살짝 바꾸는 건 안 된다.

바이트의 나열, UTF-8 인코딩

Go 에서 문자열은 사실 바이트들의 묶음 이다. 그리고 그 바이트들은 UTF-8 로 인코딩돼 있다.

  • 영문, 숫자, 기호 같은 ASCII 문자는 1바이트
  • 한글, 한자, 이모지 같은 문자는 2~4바이트

이 사실이 곧 이번 장의 함정들로 이어진다.

큰따옴표 vs 백틱

문자열 리터럴은 두 가지 방법으로 적을 수 있다.

s1 := "Hello\n World"   // 큰따옴표
s2 := `Hello\n World`   // 백틱 (raw string)
큰따옴표 "..."백틱 `...`
이스케이프 문자 해석 (\n, \t 등)이스케이프 해석 안 함
한 줄만 가능여러 줄 가능
s1 := "줄1\n줄2"        // "줄1" + 줄바꿈 + "줄2"
s2 := `줄1\n줄2`        // 그대로 "줄1\n줄2"

s3 := `여러
줄에 걸친
문자열`

정규식, JSON 템플릿, SQL 같이 역슬래시가 많이 나오는 문자열은 백틱이 편하다.


6.2 연결과 비교

+ 로 연결

문자열은 + 로 이어붙일 수 있다.

first := "Hello"
last  := "World"
msg   := first + ", " + last + "!"
fmt.Println(msg)  // Hello, World!

+= 로 누적도 가능하다.

s := ""
s += "안녕"
s += "하세요"
fmt.Println(s)  // 안녕하세요

짧은 연결은 + 로 충분하지만, 반복문 안에서 수백 번 이어붙여야 한다면 strings.Builder 가 훨씬 효율적이다. (26장에서)

비교

문자열도 5장의 비교 연산자를 그대로 쓴다.

fmt.Println("apple" == "apple")  // true
fmt.Println("apple" != "Apple")  // true (대소문자 다름)
fmt.Println("apple" < "banana")  // true (사전식)

비교는 바이트 단위로 이뤄진다. 한글도 마찬가지지만, “가나다 순으로 정확히 정렬되느냐” 는 더 복잡한 이야기다. 지금은 “같은가 다른가” 비교 정도만 안전하다고 보면 된다.


6.3 길이 구하기

len() 은 바이트 단위

문자열의 길이는 len() 함수로 구한다.

fmt.Println(len("hello"))  // 5

여기까지는 직관과 같다. 하지만 한글이 들어가면 결과가 달라진다.

fmt.Println(len("안녕"))  // 6

한글 한 글자가 UTF-8 에서 3바이트를 차지하기 때문이다. "안녕" 은 2글자지만 6바이트다.

len(s) 은 글자 수가 아니라 바이트 수다.

영문 한 글자는 1바이트, 한글 한 글자는 3바이트라는 점을 잊지 말자.

글자 수가 필요할 땐?

진짜 문자 단위 길이는 []rune 으로 변환해서 구한다.

s := "안녕Go"
fmt.Println(len(s))           // 8 (3 + 3 + 1 + 1)
fmt.Println(len([]rune(s)))   // 4 (안, 녕, G, o)

rune 의 정체는 잠시 뒤 6.5 절에서 다룬다.


6.4 인덱싱과 슬라이싱

s[i] 는 byte 를 돌려준다

문자열에 대괄호로 인덱스를 주면 그 위치의 바이트 를 돌려준다.

s := "hello"
fmt.Println(s[0])    // 104 (소문자 'h' 의 ASCII 코드값)

문자열을 인덱싱하면 글자가 아니라 숫자가 나오는 게 처음엔 어색하다. “인덱싱 = 바이트 꺼내기” 라고 기억하자.

문자처럼 보고 싶다면 변환이 필요하다.

fmt.Println(string(s[0]))   // "h"

슬라이싱

s[i:j] 형태로 부분 문자열을 잘라낼 수 있다.

s := "Hello, World!"
fmt.Println(s[0:5])    // "Hello"
fmt.Println(s[7:12])   // "World"
fmt.Println(s[:5])     // "Hello"  (앞부터 5바이트)
fmt.Println(s[7:])     // "World!" (7번째부터 끝까지)

여기서도 단위는 글자가 아니라 바이트 다.

한글 인덱싱의 함정

영문 문자열은 한 글자 = 한 바이트라 인덱싱이 직관적이다. 하지만 한글은 그렇지 않다.

s := "안녕"
fmt.Println(s[0])  // 236 (한글 첫 글자의 첫 바이트)
fmt.Println(s[1])  // 149 (둘째 바이트)
fmt.Println(s[2])  // 136 (셋째 바이트, 여기까지가 '안' 한 글자)

s[0]'안' 글자 통째가 아니다. '안' 을 구성하는 3바이트 중 첫 번째 바이트일 뿐이다.

그래서 한글 문자열을 잘못 슬라이싱하면 글자가 깨진다.

s := "안녕하세요"
fmt.Println(s[0:1])   // 깨진 문자
fmt.Println(s[0:3])   // "안"

[0:3] 처럼 글자 경계에 딱 맞춰 잘라야 온전한 글자가 된다. 이게 다음 6.5 절에서 rune 이 필요한 이유로 이어진다.


6.5 byte 와 rune

두 타입의 정체

4장에서 살짝 봤다.

  • byte = uint8 (1바이트 정수)
  • rune = int32 (4바이트 정수, 유니코드 코드 포인트)

각각 다른 목적으로 쓴다.

타입용도
byte“한 바이트” 를 다룰 때
rune“한 글자(유니코드)” 를 다룰 때

[]rune 으로 변환

문자열을 글자 단위로 다루려면 []rune 으로 변환한다.

s := "안녕Go"

bs := []byte(s)
rs := []rune(s)

fmt.Println(len(bs))   // 8 (바이트 개수)
fmt.Println(len(rs))   // 4 (글자 개수)

fmt.Println(string(rs[0]))   // "안"
fmt.Println(string(rs[1]))   // "녕"
fmt.Println(string(rs[2]))   // "G"
fmt.Println(string(rs[3]))   // "o"

rs[0]'안' 한 글자 전체에 해당하는 코드 포인트(정수) 다. string() 으로 다시 감싸면 우리가 아는 문자가 된다.

글자 단위 작업 패턴

한글이 섞인 문자열을 다룰 때는 보통 이렇게 한다.

s := "안녕하세요"

rs := []rune(s)
fmt.Println(len(rs))          // 5 (글자 수)
fmt.Println(string(rs[:3]))   // "안녕하"
fmt.Println(string(rs[2:]))   // "하세요"

“바이트로 다룰지, 글자로 다룰지” 만 의식하면 한글 처리도 어렵지 않다.

range 로 글자 순회 (짧은 언급)

반복문 for ... range 로 문자열을 돌면 바이트가 아니라 rune 단위 로 순회한다.

for i, r := range "안녕Go" {
    fmt.Println(i, string(r))
}

for 문 자체는 다음 8장에서 본격적으로 다룬다. 지금은 “range 로 돌리면 글자 단위” 라는 것만 기억해 두자.


6.6 정리

  • 문자열은 불변 이며 UTF-8 바이트의 나열 이다
  • 큰따옴표는 이스케이프 해석, 백틱은 원문 그대로(raw)
  • 연결은 +, 비교는 ==, < 등으로 한다
  • len(s)바이트 수 다 — 한글 한 글자는 3바이트
  • s[i] 인덱싱은 바이트 를 돌려준다
  • 글자 단위로 다루려면 []rune 으로 변환

이 정도면 일상적인 문자열 처리는 가능하다. 검색, 치환, 분리 같은 본격적인 기능은 strings 패키지에 들어 있다. 표준 라이브러리를 다루는 27장에서 다시 만난다.

다음 장에서는 그동안 무심코 써 온 fmt.Println 의 정체를 들여다본다. 출력 형식을 마음대로 조절하고, 사용자 입력도 받아 본다.